feat(web): JWT session versioning and credential revocation on org removal#1168
feat(web): JWT session versioning and credential revocation on org removal#1168brendan-kellam merged 4 commits intomainfrom
Conversation
…moval Adds a per-user `sessionVersion` integer to the `User` model. The version is baked into every newly-minted JWT cookie via the `jwt` callback, copied onto the session via the `session` callback, and verified on every read by a wrapped `auth()` function that compares the cookie's claim against the current DB value — mismatch returns null, treating the session as logged out on the very next request. Backwards compatible: pre-migration cookies have no claim and fall back to 0, which matches the default User.sessionVersion of 0, so existing sessions keep working until something explicitly bumps the user's version. The `auth()` wrapper is memoized per-request via React `cache()` so the extra DB read happens at most once per request even though `auth()` is called from many places (layout, page, withAuth, getAuthenticatedUser). `removeMemberFromOrg` and `leaveOrg` now run three credential-revocation helpers inside the existing serializable transaction: - `invalidateAllSessionsForUser` — bumps the version, killing every active JWT cookie for the user on their next request. - `revokeUserOAuthTokens` — deletes their `OAuthToken`, `OAuthRefreshToken`, and `OAuthAuthorizationCode` rows. Not org-scoped because OAuthClient has no `orgId`. - `revokeUserApiKeysInOrg` — deletes their `ApiKey` rows scoped to the current org (ApiKey.orgId). Net effect: when an admin removes a member (or a member leaves), the user's JWT cookie, personal API keys for that org, and OAuth tokens all stop working atomically. A failed transaction rolls back all four changes.
This comment has been minimized.
This comment has been minimized.
WalkthroughAdds per-user sessionVersion (DB + Prisma) used in JWTs; NextAuth propagates and validates the claim via a cached auth() that returns null on mismatch. removeMemberFromOrg/leaveOrg now increment sessionVersion and revoke a user’s OAuth tokens and org API keys inside the same Prisma transaction. CHANGELOG and mocks updated. ChangesPer-User JWT Session Versioning & Membership Revocation
Sequence DiagramsequenceDiagram
participant Client
participant NextAuth as NextAuth<br/>(middleware)
participant Auth as auth()<br/>(cache)
participant DB as Database
participant API as Protected<br/>Route
Client->>NextAuth: Request with JWT cookie (sessionVersion)
NextAuth->>Auth: call auth()
Auth->>DB: fetch user by sub
DB-->>Auth: user (sessionVersion = X)
alt X == JWT.sessionVersion
Auth-->>NextAuth: session object
NextAuth->>API: forward request (authorized)
API-->>Client: 200 OK
else mismatch
Auth-->>NextAuth: null
NextAuth-->>Client: 401 Unauthorized
end
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly Related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@CHANGELOG.md`:
- Around line 10-11: The changelog entry currently under "### Added" describing
per-user JWT session versioning is a fix, not a new feature; move the entire
bullet ("Added per-user JWT session versioning so admin-driven member
removals... [`#1168`](https://github.com/sourcebot-dev/sourcebot/pull/1168)") out
of the "### Added" section and append it to the bottom of the "### Fixed"
section, preserving the exact text and PR link and leaving other entries/order
unchanged.
In `@packages/web/src/features/userManagement/actions.ts`:
- Around line 130-161: The new revocation functions revokeUserApiKeysInOrg and
revokeUserOAuthTokens perform unindexed deleteMany queries; add appropriate
indexes to avoid full-table scans by updating the Prisma schema: add a composite
index on ApiKey for (createdById, orgId) and add single-column indexes on
OAuthToken.userId, OAuthRefreshToken.userId, and OAuthAuthorizationCode.userId
(or composite if you prefer specific access patterns), then generate and apply a
migration so the deleteMany calls run against indexed columns within the
transaction.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 21dca91d-f46d-4355-a889-59c628f6d5d5
📒 Files selected for processing (6)
CHANGELOG.mdpackages/db/prisma/migrations/20260501170139_add_user_session_version/migration.sqlpackages/db/prisma/schema.prismapackages/web/src/__mocks__/prisma.tspackages/web/src/auth.tspackages/web/src/features/userManagement/actions.ts
This PR adds a
sessionVersioninteger to the User table and JWT token. Wheneverauthis called, we compare the version stored in the database and the token. If they don't match, then the session is invalid (null). This allows us to invalidate JWT tokens from the server by incrementing thesessionVersionfor a given user.We use this capability in the user removal path to sign out users when the admin removes them from the organization.
Test plan
/api/repos, confirm 200.auth()returns null,withAuthrejects, page redirects to/login.ApiKeyrow is gone).sessionVersion.leaveOrg— same cascade behavior when a non-owner leaves voluntarily.leaveOrg— last-owner guard still rejects the action.sessionVersionclaim and fall back to 0).🤖 Generated with Claude Code
Summary by CodeRabbit